/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
import {ArkFile, ts} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {BaseChecker, BaseMetaData} from "../BaseChecker";
import {Defects, Utils} from "../../Index";
import {FileMatcher, MatcherCallback, MatcherTypes} from "../../Index";
import {Rule} from "../../Index";
import {RuleListUtil} from "../../utils/common/DefectsList";
import {IssueReport} from "../../model/Defects";
import {RuleFix} from "../../model/Fix";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'ConsistentTypeImportsCheck');

const gMetaData: BaseMetaData = {
    severity: 2,
    ruleDocPath: "docs/consistent-type-imports-check.md",
    description: "Enforce consistent usage of type imports."
};

enum TypeImportsFixStyle {
    inline = 0,
    separate = 1
}

enum TypeImportsPrefer {
    noType = 0,
    type = 1
}

type RuleOptions = {
    disallowTypeAnnotations: boolean,
    fixStyle: TypeImportsFixStyle,
    prefer: TypeImportsPrefer
}

type Options = {
    disallowTypeAnnotations: boolean,
    fixStyle: string,
    prefer: string
}

export class ConsistentTypeImportsCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];
    private issueMap: Map<string, IssueReport> = new Map();
    private typeMessage = 'All imports in the declaration are only used as types. Use `import type`.';
    private noTypeMessage = 'Use an `import` instead of an `import type`.';
    private disallowTypeAnnotationsMessage = '`import()` type annotations are forbidden.';
    private ruleOptions: RuleOptions = { disallowTypeAnnotations: true, fixStyle: TypeImportsFixStyle.separate, prefer: TypeImportsPrefer.type }

    private fileMatcher: FileMatcher = {
        matcherType: MatcherTypes.FILE
    };

    public registerMatchers(): MatcherCallback[] {
        const fileMatchBuildCb: MatcherCallback = {
            matcher: this.fileMatcher,
            callback: this.check
        }
        return [fileMatchBuildCb];
    }

    public check = (targetFile: ArkFile) => {
        if (!targetFile.getFilePath().endsWith(".ts")) {
            return;
        }

        let options = this.rule.option;
        if (options.length > 0) {
            const option = options[0] as Options;
            this.ruleOptions.disallowTypeAnnotations = option.disallowTypeAnnotations;
            if (option.fixStyle == 'inline-type-imports') {
                this.ruleOptions.fixStyle = TypeImportsFixStyle.inline;
            } else if (option.fixStyle == 'separate-type-imports') {
                this.ruleOptions.fixStyle = TypeImportsFixStyle.separate;
            }
            if (option.prefer == 'no-type-imports') {
                this.ruleOptions.prefer = TypeImportsPrefer.noType;
            } else if (option.prefer == 'type-imports') {
                this.ruleOptions.prefer = TypeImportsPrefer.type;
            }
        }

        const sourceFile = ts.createSourceFile('temp.ts', targetFile.getCode(), ts.ScriptTarget.Latest, true);
        const sourceFileObject = ts.getParseTreeNode(sourceFile);
        if (sourceFileObject == undefined) {
            return;
        }

        this.loopNode(targetFile, sourceFile, sourceFileObject);

        this.reportSortedIssues();
    }

    public loopNode(targetFile: ArkFile, sourceFile: ts.SourceFileLike, aNode: ts.Node) {
        const children = aNode.getChildren();
        for (const child of children) {
            if (ts.isImportDeclaration(child)) { // prefer 规则
                this.checkImport(sourceFile, child, targetFile);
            } else if (ts.isTypeAliasDeclaration(child)) { // disallowTypeAnnotations 规则：type T = import('Foo').Foo;
                this.checkTypeAlias(sourceFile, child, targetFile);
            } else if (ts.isVariableStatement(child)) { // disallowTypeAnnotations 规则：let foo: import('foo');
                this.checkVariableStatement(sourceFile, child, targetFile);
            } else {
                this.loopNode(targetFile, sourceFile, child);
            }
        }
    }

    private checkImport(sourceFile: ts.SourceFileLike, node: ts.Node, targetFile: ArkFile) {
        const nodeList = node.getChildren();
        if (nodeList.length < 3) {
            return;
        }

        let second = nodeList[1];
        if (!ts.isImportClause(second)) { // type { Foo } 或 { Foo }
            return;
        }

        // 找出第一个节点是否是 type
        const secondNodeList = second.getChildren();
        if (secondNodeList.length < 1) {
            return;
        }

        const secondFirst = secondNodeList[0];
        const hasType = secondFirst.kind == ts.SyntaxKind.TypeKeyword;
        if ((this.ruleOptions.prefer == TypeImportsPrefer.noType && hasType) ||
            (this.ruleOptions.prefer == TypeImportsPrefer.type && !hasType)) {

            let message;
            let fix: RuleFix | undefined;
            const first = nodeList[0];
            if (this.ruleOptions.prefer == TypeImportsPrefer.noType && hasType) {
                message = this.noTypeMessage;

                fix = {range: [first.getStart(), secondFirst.getEnd()], text: first.getText()};
            } else {
                message = this.typeMessage;

                const secondText = second.getText();
                if (secondText.charAt(0) == '{' && this.ruleOptions.fixStyle == TypeImportsFixStyle.inline) {
                    const startPosition = ts.getLineAndCharacterOfPosition(sourceFile, second.getStart());

                    const position = ts.getPositionOfLineAndCharacter(sourceFile, startPosition.line, startPosition.character + 1);
                    fix = {range: [second.getStart(), position], text: "{ type "};
                } else {
                    fix = {range: [first.getStart(), first.getEnd()], text: first.getText() + ' type'};
                }
            }

            const startPosition = ts.getLineAndCharacterOfPosition(sourceFile, node.getStart());
            const defect = this.addIssueReport(targetFile, startPosition.line + 1, startPosition.character + 1, 0, message, fix);
            this.issueMap.set(defect.fixKey, {defect, fix});
        }
    }

    private checkTypeAlias(sourceFile: ts.SourceFileLike, node: ts.Node, targetFile: ArkFile) {
        const nodeList = node.getChildren();
        for (const child of nodeList) {
            if (ts.isImportTypeNode(child) && this.ruleOptions.disallowTypeAnnotations) {
                const startPosition = ts.getLineAndCharacterOfPosition(sourceFile, child.getStart());
                this.addIssueReport(targetFile, startPosition.line + 1, startPosition.character + 1, 0, this.disallowTypeAnnotationsMessage, undefined);
                break;
            }
        }
    }

    private checkVariableStatement(sourceFile: ts.SourceFileLike, node: ts.Node, targetFile: ArkFile) {
        const nodeList = node.getChildren();
        for (const child of nodeList) {
            if (ts.isImportTypeNode(child) && this.ruleOptions.disallowTypeAnnotations) {
                const startPosition = ts.getLineAndCharacterOfPosition(sourceFile, child.getStart());
                this.addIssueReport(targetFile, startPosition.line + 1, startPosition.character + 1, 0, this.disallowTypeAnnotationsMessage, undefined);
                break;
            } else {
                this.checkVariableStatement(sourceFile, child, targetFile);
            }
        }
    }

    private addIssueReport(arkFile: ArkFile, line: number, startCol: number, endCol: number, message: string, fix: RuleFix | undefined): Defects {
        const severity = this.rule.alert ?? this.metaData.severity;
        const filePath = arkFile.getFilePath();
        const fixKey = line + '%' + startCol + '%' + this.rule.ruleId;
        const mergeKey = filePath + '%' + fixKey + '%' + this.metaData.description;
        const defect = new Defects(line, startCol, message, severity, this.rule.ruleId,
            mergeKey, this.metaData.ruleDocPath, true, false, false, fixKey);
        this.issues.push(new IssueReport(defect, fix));
        RuleListUtil.push(defect);
        return defect;
    }

    private reportSortedIssues(): void {
        if (this.issueMap.size === 0) {
            return;
        }

        const sortedIssues = Array.from(this.issueMap.entries())
            .sort(([keyA], [keyB]) => Utils.sortByLineAndColumn(keyA, keyB));

        this.issues = [];

        sortedIssues.forEach(([_, issue]) => {
            RuleListUtil.push(issue.defect);
            this.issues.push(issue);
        });
    }
}